import java.io.Serializable;
import java.math.BigDecimal;
import java.math.RoundingMode;

import javax.swing.JOptionPane;

/**
 * Copyright (C) 2013  Arun Lee & Bryce Nagareda
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

/**
 * Calculates the math for the Horizontal sundial.
 * 
 * @author Arun Lee
 * @author briceorbryce
 *
 */
public class SundialMath implements Serializable {
	
	/**Serial version UID*/
	private static final long serialVersionUID = 1L;
	
	/**Latitude the user selected.*/
	protected double latitude;
	/**Longitude the user selected.*/
	protected double longitude;
	/**Day the user selected.*/
	protected int day;
	/**Month the user selected.*/
	protected int month;
	/**The hour lines to be drawn on the GUI.*/
	protected double[] HourLines;
	/**The time zone of the current user.*/
	protected TimeZone tz;
	
	/**
	 * Calls the super class
	 */
	public SundialMath(){
		super();
	}
	
	/**
	 * Creates a new Sundialmath object. If the day or the month is invalid,
	 * throw an exception.
	 * 
	 * @param latitude the user inputted
	 * @param longitude the user inputted
	 * @param day the user selected
	 * @param month the user selected
	 * @throws MonthDayException if user selects invalid month and day, fail
	 */
	public SundialMath(double latitude, double longitude, int day, String month) throws MonthDayException {
		this.latitude = latitude;
		this.longitude = longitude;
		checkMonthDay(month, day);
		this.HourLines = new double[25];
		this.tz = new TimeZone(Double.toString(this.latitude), Double.toString(this.longitude));
		if(this.day > 0 && checkLatLon()){
			calculateHourLines();
		}
		else {
			throw new MonthDayException("Day or lat or long is wrong.");
		}
	}
	
	/**
	 * Calculates the hour lines.
	 */
	protected void calculateHourLines(){
		BigDecimal temp;
		double sMeridian = 0;
		double sunrise = 6;
		
		if(this.tz.getTimeZoneName() != null){
			sMeridian = this.tz.getRawOffset();
			sMeridian = sMeridian / 3600 * 15;
		
			//Every degree is 4 mins so * 4, divide by 60 so we get the time in hours
			if(this.longitude > sMeridian){	//meaning the location is east of the Standard Meridian
				sunrise = (sunrise + ((this.longitude - sMeridian) * 4) / 60);
			}
			else {	//meaning the location is west of the Standard Meridian
				sunrise = (sunrise - ((sMeridian - this.longitude) * 4) / 60);
			}
		}
		
		//apply equation of time correction
		sunrise = sunrise - EOTCorrection();
		
		for(int i = 0; i < this.HourLines.length; i++){
			this.HourLines[i] = Math.toDegrees(Math.atan(Math.sin(Math.toRadians(Math.abs(this.latitude)))*(Math.tan(Math.toRadians((sunrise-12)*15)))));
			temp = BigDecimal.valueOf(this.HourLines[i]);
			temp = temp.setScale(3, RoundingMode.HALF_UP);
			this.HourLines[i] = temp.doubleValue(); 
			sunrise = sunrise + 0.5;
		}
	}
	
	/**
	 * EOTCorrection - calculate the equation of time using information from http://www.susdesign.com/popups/sunangle/eot.php
	 * @return double - the correction in hour
	 */
	protected double EOTCorrection(){
		double correction = 0;
		
		double B = 360 * ((double)getDayNumber() - 81) / 365;
		if(B !=  360 * ((0.0 - 81) / 365)){
			correction = 9.87 * Math.sin(Math.toRadians(2*B)) - 7.53 * Math.cos(Math.toRadians(B)) - 1.5 * Math.sin(Math.toRadians(B));
		}
		return correction/60;
	}
	
	/**
	 * getDayNumber - calculate the number of given day 
	 * @return int - the day number
	 */
	protected int getDayNumber(){
		//variables
		int daynum = 0;
		//number of days in each month and months
		int[] daysofmonth = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
		
		//Check if month and day are valid
		if(this.month >= 1 && this.month <= 12 && this.day <= daysofmonth[this.month-1] && this.day > 0){
			if(this.month == 1){
					daynum = this.day;
			}
			else {
				for(int i = 0; i < this.month - 1; i++){
					daynum = daynum + daysofmonth[i];
				}
				daynum = daynum + this.day;
			}
		}
		else {
			JOptionPane.showMessageDialog(null, "The month or day is invalid.\nA Sundial without equation of time correction will be created.");
		}
		return daynum;
	}
	
	
	/**
	 * Checks the month and day to make sure they are valid.
	 * 
	 * @param m month chosen
	 * @param d day chosen
	 */
	protected void checkMonthDay(String m, int d) throws MonthDayException {
		int[] daysofmonth = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
		
		checkMonth(m);
		
		if(d > daysofmonth[this.month-1]){
			this.day = 0;
			throw new MonthDayException("The day is invalid.");
		}
		this.day = d;
	}
	
	/**
	 * Checks to make sure the user or the "last run" file have a valid month.
	 * 
	 * @param monthToCheck the month
	 * @throws MonthDayException thrown if the file doesn't have a valid month
	 */
	protected void checkMonth (String monthToCheck) throws MonthDayException {
//		switch (monthToCheck) {
//		case "January":
//			this.month = 1;
//			break;
//		case "February":
//			this.month = 2;
//			break;
//		case "March":
//			this.month = 3;
//			break;
//		case "April":
//			this.month = 4;
//			break;
//		case "May":
//			this.month = 5;
//			break;
//		case "June":
//			this.month = 6;
//			break;
//		case "July":
//			this.month = 7;
//			break;
//		case "August":
//			this.month = 8;
//			break;
//		case "September":
//			this.month = 9;
//			break;
//		case "October":
//			this.month = 10;
//			break;
//		case "November":
//			this.month = 11;
//			break;
//		case "December":
//			this.month = 12;
//			break;
//		default:
//			throw new MonthDayException("Month is wrong.");
//		}
		if(monthToCheck.equals("January")){
			this.month = 1;
		}else if(monthToCheck.equals("February")){
			this.month = 2;
		}else if(monthToCheck.equals("March")){
			this.month = 3;
		}else if(monthToCheck.equals("April")){
			this.month = 4;
		}else if(monthToCheck.equals("May")){
			this.month = 5;
		}else if(monthToCheck.equals("June")){
			this.month = 6;
		}else if(monthToCheck.equals("July")){
			this.month = 7;
		}else if(monthToCheck.equals("August")){
			this.month = 8;
		}else if(monthToCheck.equals("September")){
			this.month = 9;
		}else if(monthToCheck.equals("October")){
			this.month = 10;
		}else if(monthToCheck.equals("November")){
			this.month = 11;
		}else if(monthToCheck.equals("December")){
			this.month = 12;
		}
		else {
			throw new MonthDayException("Month is wrong");
		}
	}
	
	/**
	 * checkLatLon - Check if the latitude and longitude are possible value
	 * 
	 *  @param lat Latitude 
	 *  @param lon Longitude
	 */
	protected boolean checkLatLon(){
		if(this.latitude <= 90 && this.latitude >= -90 && this.latitude != 0){
			if(this.longitude <= 180 && this.longitude >= -180 && this.longitude != 0){
				return true;
			}
		}
		return false;
	}
	
	/**
	 * Getter for latitude. Will be used in HSundialMath.
	 * @return latitude
	 */
	public int getLatitude(){
		return (int)this.latitude;
	}
	
	/**
	 * Getter for latitude. Used with show previous sundial.
	 * @return latitude for viewer
	 */
	public double getDoubleLatitude() {
		return this.latitude;
	}
	
	/**
	 * Getter for longitude. Used with show previous sundial.
	 * @return longitude
	 */
	public double getDoubleLongitude() {
		return this.longitude;
	}
	
	/**
	 * Getter for longitude. Will be used for HSundialMath
	 * @return latitude
	 */
	public int getLongitude(){
		return (int)this.longitude;
	}
	
	/**
	 * Getter for the day
	 * @return the day
	 */
	public int getDay(){
		return this.day;
	}
	
	/**
	 * Getter for the month
	 * @return the month
	 */
	public int getMonth(){
		return this.month;
	}
	/**
	 * Getter for the hour lines array.
	 * @return the hour lines array
	 */
	public double[] getHourLines(){
		return this.HourLines;
	}
	
	/**
	 * Getter for timezone object.
	 * @return timezone object
	 */
	public TimeZone getTimeZone() {
		return this.tz;
	}
	
	/**
	 * Returns the time zone the user selected or "unknown timezone" if null.
	 * @return timezone name
	 */
	public String getTimeZoneName(){
		if(this.tz.getTimeZoneName() == null){
			return "Unknown TimeZone";
		}
		return this.tz.getTimeZoneName();
	}
	
	/**
	 * main method to test - but shouldn't be used lol
	 * @param args not used
	 */
	public static void main(String[] args ){
		SundialMath test;
		try {
			test = new SundialMath(211, -157, 15, "April");
			System.out.println("Lat: " + test.getLatitude() + " Long: " + test.getLongitude() + " Day: "+ test.getDay() + " Month: " + test.getMonth());
			System.out.println("Timezone: " + test.getTimeZoneName());
			double[] hr = test.getHourLines();
			double index = 6;
			for(int i = 0; i < hr.length; i++){
				System.out.print(index);
				System.out.print(": ");
				System.out.println(hr[i]);
				index = index + 0.5;
			}
		} catch (MonthDayException e) {
			System.out.println(e);
		}
	}
}
